<?php

namespace sessions;


ini_set('display_errors',1);
ini_set('display_startup_errors',1);
error_reporting(-1);

// MADE WITH HELP OF PARSCLICK VIDEO: https://www.youtube.com/watch?v=IXUD67JNTfM
/*
 * This file is part of the Symfony package.
 *
 * (c) Fabien Potencier <fabien@symfony.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

//namespace Symfony\Component\HttpFoundation\Session\Storage\Handler;


/**
 * Session handler using a PDO connection to read and write data.
 *
 * It works with MySQL, PostgreSQL, Oracle, SQL Server and SQLite and implements
 * different locking strategies to handle concurrent access to the same session.
 * Locking is necessary to prevent loss of data due to race conditions and to keep
 * the session data consistent between read() and write(). With locking, requests
 * for the same session will wait until the other one finished writing. For this
 * reason it's best practice to close a session as early as possible to improve
 * concurrency. PHPs internal files session handler also implements locking.
 *
 * Attention: Since SQLite does not support row level locks but locks the whole database,
 * it means only one session can be accessed at a time. Even different sessions would wait
 * for another to finish. So saving session in SQLite should only be considered for
 * development or prototypes.
 *
 * Session data is a binary string that can contain non-printable characters like the null byte.
 * For this reason it must be saved in a binary column in the database like BLOB in MySQL.
 * Saving it in a character column could corrupt the data. You can use createTable()
 * to initialize a correctly defined table.
 *
 * @see http://php.net/sessionhandlerinterface
 *
 * @author Fabien Potencier <fabien@symfony.com>
 * @author Michael Williams <michael.williams@funsational.com>
 * @author Tobias Schultze <http://tobion.de>
 */



//class PdoSessionHandler implements \SessionHandlerInterface
class MysqlSessionHandler implements \SessionHandlerInterface
{
    protected $db;
    protected $useTransactions;
    protected $expiry;
    protected $table_sess = 'sessions';
    protected $col_sid = 'sid';
    protected $col_expiry = 'expiry';
    protected $col_data = 'data';
    protected $col_ukey = 'user_key';
    protected $unlockStatements = [];
    protected $collectGarbage = false;
    protected $sess_ukey = 'user_key';
 
   
    /**
     * Constructor.
     *
     * You can either pass an existing database connection as PDO instance or
     * pass a DSN string that will be used to lazy-connect to the database
     * when the session is actually used. Furthermore it's possible to pass null
     * which will then use the session.save_path ini setting as PDO DSN parameter.
     *
     * List of available options:
     *  * db_table: The name of the table [default: sessions]
     *  * db_id_col: The column where to store the session id [default: sess_id]
     *  * db_data_col: The column where to store the session data [default: sess_data]
     *  * db_lifetime_col: The column where to store the lifetime [default: sess_lifetime]
     *  * db_time_col: The column where to store the timestamp [default: sess_time]
     *  * db_username: The username when lazy-connect [default: '']
     *  * db_password: The password when lazy-connect [default: '']
     *  * db_connection_options: An array of driver-specific connection options [default: array()]
     *  * lock_mode: The strategy for locking, see constants [default: LOCK_TRANSACTIONAL]
     *
     * @param \PDO|string|null $pdoOrDsn A \PDO instance or DSN string or null
     * @param array            $options  An associative array of options
     *
     * @throws \InvalidArgumentException When PDO error mode is not PDO::ERRMODE_EXCEPTION
     */



    public function __construct(\PDO $db, $useTransactions = true)
    {
        $this->db = $db;
        if ($this->db->getAttribute(\PDO::ATTR_ERRMODE) !== \PDO::ERRMODE_EXCEPTION){
            $this->db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
        }
        $this->useTransactions = $useTransactions;
        $this->expiry = time() + (int) ini_get('session.gc_maxlifetime');
    }

    
    /**
     * Opens the session

     */
    public function open($save_path, $name)
    {
        return true;
    }

    /**
     * reads the session data
     */

    public function read($session_id)
    {
     
     try{
        if ($this->useTransactions) {
            //mysql default isolation, repeatable read, causes deadlock for different sessions
            $this->db->exec('SET TRANSACTION ISOLATION LEVEL READ COMMITTED');
            $this->db->beginTransaction();
        } else {
            $this->unlockStatement[] = $this->getLock($session_id);
        }

        $sql = "SELECT $this->col_expiry, $this->col_data FROM 
        $this->table_sess WHERE $this->col_sid = :sid";

        // when using a transaction, SELECT FOR UPDATE is necessary
        // to avoid deadlock of connection that starts reading
        // before we write

        if ($this->useTransactions){
            $sql .= ' FOR UPDATE';
        }

        $selectStmt = $this->db->prepare($sql);
        $selectStmt->bindParam(':sid', $session_id);//klopt
        $selectStmt->execute();
        $results = $selectStmt->fetch(\PDO::FETCH_ASSOC);
        if ($results) {
            if ($results[$this->col_expiry] < time()) {
                // return and empty string if data out of date
                return '';
            }
            return $results[$this->col_data];
        }
        // well get this far only if there are not results, which means
        // the session hasnt been registered in the db
        if ($this->useTransactions){
            $this->initializeRecord($selectStmt);
        }
        // return an empty array string if transactions arent being used
        // and the sesssion hasnt been registered in the db
        return '';
     } catch (\PDOException $e){
        if ($this->db->inTransaction()) {
            $this->db->rollBack();
        } 
        throw $e;
     }


    }

    /* registers new session ID in the db when using transactions

    exclusive reading of non existent row does not block, so we need to insert a row until the transaction is committed

    */


    /**
     * {@inheritdoc}
     */

    public function write($session_id, $data)
    {
          try {
            $sql ="INSERT INTO $this->table_sess (
            $this->col_sid, $this->col_expiry, $this->col_data, $this->col_ukey)    
            VALUES (:sid, :expiry, :data, :key) 
            ON DUPLICATE KEY UPDATE $this->col_expiry = :expiry, $this->col_data = :data";//editted for serverlog purposes
        $stmt = $this->db->prepare($sql);
        $stmt->bindParam(':sid', $session_id);
        $stmt->bindParam(':expiry', $this->expiry, \PDO::PARAM_INT);
        $stmt->bindParam(':data', $data);
        $stmt->bindParam(':key', $_SESSION[$this->sess_ukey]);//self added for serverlog purposes
        $stmt->execute();
        return true;                   
               } catch (\PDOException $e){
        if ($this->db->inTransaction()) {
            $this->db->rollBack();
        } 
        throw $e;
    }
}

    /**
     * {@inheritdoc}
     */
    public function close()
    {
        if ($this->db->inTransaction()) {
            $this->db->commit();
        } elseif ($this->unlockStatements) {
            while ($unlockStmt = array_shift($this->unlockStatements)) {
                $unlockStmt->execute();
            }
        }

        if ($this->collectGarbage) {
            $sql = "DELETE FROM $this->table_sess 
            WHERE $this->col_expiry < :time";
            $stmt = $this->db->prepare($sql);
            $stmt->bindValue(':time', time(), \PDO::PARAM_INT);//klopt
            $stmt->execute();
            $this->collectGarbage = false;

        }

        return true;
    }

    /**
     * destroy the session
     */
    public function destroy($session_id)
    {
        // delete the record associated with this id
        $sql = "DELETE FROM $this->table_sess WHERE $this->col_sid = :sid";
        try {
            $stmt = $this->db->prepare($sql);
            $stmt->bindParam(':sid', $session_id);//klopt
            $stmt->execute();
        } catch (\PDOException $e) {
            if ($this->db->inTransaction()) {
                $this->db->rollBack();
            } 
            throw $e;
        }
        return true;
    }

    /**
     * Garbage collection
     */
    public function gc($maxlifetime)
    {

        $this->collectGarbage = true;
        return true;

    }

     /* executes an application level lock on the db

    */

    protected function getLock($session_id)
    {
        $stmt = $this->db->prepare('SELECT GET_LOCK(:key,50)');
        $stmt->bindValue(':key',$session_id);//dit klopt
        $stmt->execute();

        $releaseStmt = $this->db->prepare('DO RELEASE_LOCK(:key)');
        $releaseStmt->bindValue(':key', $session_id);//dit klopt

        return $releaseStmt;
    }
   


    protected function initializeRecord(\PDOStatement $selectStmt)
    {
        try{
            $sql = "INSERT INTO $this->table_sess ($this->col_sid,$this->col_expiry,$this->col_data) VALUES (:sid, :expiry, :data)";
            $insertStmt = $this->db->prepare($sql);
            $insertStmt->bindParam(':sid', $session_id);//klopt
            $insertStmt->bindParam(':expiry', $this->expiry, \PDO::PARAM_INT);//klopt
            $insertStmt->bindValue(':data', '');//klopt
            $insertStmt->execute();
            return '';
        } catch (\PDOException $e){
            // catch duplicate keyy error if session has already been created
            if (0 === strpos($e->getCode(), '23')) {
                // retrieve existing session data written by the current connection
                $selectStmt->execute();
                $results = $selectStmt->fetch(\PDO::FETCH_ASSOC);
                if ($results) {
                    return $results[$this->col_data];
                }
                return '';
            }
            //rollback transacion if the error was caused by something else
            if ($this->db->inTransaction()) {
                $this->db->rollBack();
            } 
            throw $e;
        }
    }
}
